// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
//
// This file generates the extension methods public API and the extension
// methods patch file for all integers, double, and float.
// The PointerPointer and PointerStruct extension are written by hand since
// those are not repetitive.

import 'dart:io';

import 'package:args/args.dart';

//
// Configuration.
//

const configuration = [
  Config("Int8", "int", "Int8List", 1),
  Config("Int16", "int", "Int16List", 2),
  Config("Int32", "int", "Int32List", 4),
  Config("Int64", "int", "Int64List", 8),
  Config("Uint8", "int", "Uint8List", 1),
  Config("Uint16", "int", "Uint16List", 2),
  Config("Uint32", "int", "Uint32List", 4),
  Config("Uint64", "int", "Uint64List", 8),
  Config("Float", "double", "Float32List", 4),
  Config("Double", "double", "Float64List", 8),
  Config("Bool", "bool", kDoNotEmit, 1, since: Version(2, 15)),
];

/// A container to generate the extension for.
class Container {
  final String name;

  /// Since annotation for the extension.
  final Version? since;

  const Container(this.name, this.since);

  static const pointer = Container('Pointer', null);
  static const array = Container('Array', Version(2, 13));

  static const typedData = Container('TypedData', Version(3, 5));

  static const values = [pointer, array, typedData];
}

//
// Generator.
//

void main() {
  update(Uri.file('sdk/lib/ffi/ffi.dart'), generatePublicExtension);
  update(
    Uri.file('sdk/lib/_internal/vm/lib/ffi_patch.dart'),
    generatePatchExtension,
  );
}

void update(Uri fileName, Function(StringBuffer, Config, Container) generator) {
  final file = File.fromUri(fileName);
  if (!file.existsSync()) {
    print('$fileName does not exist, run from the root of the SDK.');
    return;
  }

  final fileContents = file.readAsStringSync();
  final split1 = fileContents.split(header);
  if (split1.length != 2) {
    print('$fileName has unexpected contents.');
    print(split1.length);
    return;
  }
  final split2 = split1[1].split(footer);
  if (split2.length != 2) {
    print('$fileName has unexpected contents 2.');
    print(split2.length);
    return;
  }

  final buffer = StringBuffer();
  buffer.write(split1[0]);
  buffer.write(header);
  for (final container in Container.values) {
    for (final config in configuration) {
      generator(buffer, config, container);
    }
  }
  buffer.write(footer);
  buffer.write(split2[1]);

  file.writeAsStringSync(buffer.toString());
  final fmtResult = Process.runSync(Platform.resolvedExecutable, [
    "format",
    fileName.toFilePath(),
  ]);
  if (fmtResult.exitCode != 0) {
    stderr.writeln(
      "Formatting failed:\n${fmtResult.stdout}\n${fmtResult.stderr}\n",
    );
    exit(1);
  }
  print("Updated $fileName.");
}

const header = """
//
// Generated code, do not edit!
//
// Code generated by `runtime/tools/ffi/sdk_lib_ffi_generator.dart`.
//

""";

void generateHeader(StringBuffer buffer) {
  buffer.write(header);
}

void generatePublicExtension(
  StringBuffer buffer,
  Config config,
  Container container,
) {
  final nativeType = config.nativeType;
  final dartType = config.dartType;
  final typedListType = config.typedListType;
  final elementSize = config.elementSize;

  final bits = sizeOfBits(elementSize);

  String property;
  if (_isInt(nativeType)) {
    if (_isSigned(nativeType)) {
      property = "$bits-bit two's complement integer";
    } else {
      property = "$bits-bit unsigned integer";
    }
  } else if (nativeType == "Float") {
    property = "float";
  } else if (nativeType == "Double") {
    property = "double";
  } else if (nativeType == "Bool") {
    property = "bool";
  } else {
    throw "Unexpected type: $nativeType";
  }

  const platformIntPtr = """
  ///
  /// On 32-bit platforms this is a 32-bit integer, and on 64-bit platforms
  /// this is a 64-bit integer.
""";

  final platform = nativeType == "IntPtr" ? platformIntPtr : "";

  final intSignedTruncate = """
  ///
  /// A Dart integer is truncated to $bits bits (as if by `.toSigned($bits)`) before
  /// being stored, and the $bits-bit value is sign-extended when it is loaded.
""";

  const intPtrTruncate = """
  ///
  /// On 32-bit platforms a Dart integer is truncated to 32 bits (as if by
  /// `.toSigned(32)`) before being stored, and the 32-bit value is
  /// sign-extended when it is loaded.
""";

  final intUnsignedTruncate = """
  ///
  /// A Dart integer is truncated to $bits bits (as if by `.toUnsigned($bits)`) before
  /// being stored, and the $bits-bit value is zero-extended when it is loaded.
""";

  const floatTruncate = """
  ///
  /// A Dart double loses precision before being stored, and the float value is
  /// converted to a double when it is loaded.
""";

  String truncate = "";
  if (nativeType == "IntPtr") {
    truncate = intPtrTruncate;
  } else if (_isInt(nativeType) && elementSize != 8) {
    truncate = _isSigned(nativeType) ? intSignedTruncate : intUnsignedTruncate;
  } else if (nativeType == "Float") {
    truncate = floatTruncate;
  }

  final alignmentDefault = """
  ///
  /// The [address] must be ${sizeOf(elementSize)}-byte aligned.
""";

  const alignmentIntptr = """
  ///
  /// On 32-bit platforms the [address] must be 4-byte aligned, and on 64-bit
  /// platforms the [address] must be 8-byte aligned.
""";

  String alignment = "";
  if (nativeType == "IntPtr") {
    alignment = alignmentIntptr;
  } else if (elementSize != 1) {
    alignment = alignmentDefault;
  }

  final asTypedList =
      typedListType == kDoNotEmit
          ? ""
          : """
  /// Creates a typed list view backed by memory in the address space.
  ///
  /// The returned view will allow access to the memory range from [address]
  /// to `address + sizeOf<$nativeType>() * length`.
  ///
  /// The user has to ensure the memory range is accessible while using the
  /// returned list.
  ///
  /// If provided, [finalizer] will be run on the pointer once the typed list
  /// is GCed. If provided, [token] will be passed to [finalizer], otherwise
  /// the this pointer itself will be passed.
$alignment  external $typedListType asTypedList(
    int length, {
    @Since('3.1') Pointer<NativeFinalizerFunction>? finalizer,
    @Since('3.1') Pointer<Void>? token,
  });
""";
  final since =
      Version.latest(config.since, container.since)?.sinceAnnotation ?? '';
  switch (container) {
    case Container.pointer:
      buffer.write("""
/// Extension on [Pointer] specialized for the type argument [$nativeType].
$since extension ${nativeType}Pointer on Pointer<$nativeType> {
  /// The $property at [address].
$platform$truncate$alignment  external $dartType get value;

  external void set value($dartType value);

  /// The $property at `address + sizeOf<$nativeType>() * index`.
$platform$truncate$alignment  external $dartType operator [](int index);

  /// The $property at `address + sizeOf<$nativeType>() * index`.
$platform$truncate$alignment  external void operator []=(int index, $dartType value);

  /// A pointer to the [offset]th [$nativeType] after this one.
  ///
  /// Returns a pointer to the [$nativeType] whose address is
  /// [offset] times the size of `$nativeType` after the address of this pointer.
  /// That is `(this + offset).address == this.address + offset * sizeOf<$nativeType>()`.
  ///
  /// Also `(this + offset).value` is equivalent to `this[offset]`,
  /// and similarly for setting.
  @Since('3.3')
  @pragma("vm:prefer-inline")
  Pointer<$nativeType> operator +(int offset) => Pointer.fromAddress(address + sizeOf<$nativeType>() * offset);

  /// A pointer to the [offset]th [$nativeType] before this one.
  ///
  /// Equivalent to `this + (-offset)`.
  ///
  /// Returns a pointer to the [$nativeType] whose address is
  /// [offset] times the size of `$nativeType` before the address of this pointer.
  /// That is, `(this - offset).address == this.address - offset * sizeOf<$nativeType>()`.
  ///
  /// Also, `(this - offset).value` is equivalent to `this[-offset]`,
  /// and similarly for setting,
  @Since('3.3')
  @pragma("vm:prefer-inline")
  Pointer<$nativeType> operator -(int offset) => Pointer.fromAddress(address - sizeOf<$nativeType>() * offset);

$asTypedList
}

""");
    case Container.array:
      final elementsGetterReturnType =
          _isBool(nativeType) ? 'List<$dartType>' : typedListType;

      buffer.write("""
/// Bounds checking indexing methods on [Array]s of [$nativeType].
$since extension ${nativeType}Array on Array<$nativeType> {
  /// Loads a Dart value from this array at [index].
  ///
  /// This extension method must be invoked on a receiver of type `Array<T>`
  /// where `T` is a compile-time constant type.
  external $dartType operator [](int index);

  /// Stores a Dart value in this array at [index].
  ///
  /// This extension method must be invoked on a receiver of type `Array<T>`
  /// where `T` is a compile-time constant type.
  external void operator []=(int index, $dartType value);

  /// A list view of the bytes of this array.
  ///
  /// Has the same length and elements (as accessed using the index operator)
  /// as this array, and writes to either the list or this arrary are visible
  /// in both.
  @Since('3.8')
  external $elementsGetterReturnType get elements;
}

""");
    case Container.typedData:
      if (typedListType != kDoNotEmit) {
        buffer.write("""
$since extension ${typedListType}Address on $typedListType {
  /// The memory address of the underlying data.
  ///
  /// An expression of the form `expression.address` denoting this `address`
  /// can only occurr as an entire argument expression in the invocation of
  /// a leaf [Native] external function.
  ///
  /// Example:
  ///
  /// ```dart import:typed_data
  /// @Native<Void Function(Pointer<$nativeType>)>(isLeaf: true)
  /// external void myFunction(Pointer<$nativeType> pointer);
  ///
  /// void main() {
  ///   final list = $typedListType(10);
  ///   myFunction(list.address);
  /// }
  /// ```
  ///
  /// The expression before `.address` is evaluated like the left-hand-side of
  /// an assignment, to something that gives access to the storage behind the
  /// expression, which can be used both for reading and writing. The `.address`
  /// then gives a native pointer to that storage.
  ///
  /// The `.address` is evaluated just before calling into native code when
  /// invoking a leaf [Native] external function. This ensures the Dart garbage
  /// collector will not move the object that the address points in to.
  external Pointer<$nativeType> get address;
}

""");
      }
  }
}

void generatePatchExtension(
  StringBuffer buffer,
  Config config,
  Container container,
) {
  final nativeType = config.nativeType;
  final dartType = config.dartType;
  final typedListType = config.typedListType;
  final elementSize = config.elementSize;

  final sizeTimes =
      elementSize != 1 ? '${sizeOfIntPtrSize(elementSize)} * ' : '';

  final asTypedList =
      typedListType == kDoNotEmit
          ? ""
          : """
  @patch
  @pragma("vm:prefer-inline")
  $typedListType asTypedList(
    int length, {
     Pointer<NativeFinalizerFunction>? finalizer,
     Pointer<Void>? token,
  }) {
    ArgumentError.checkNotNull(this, "Pointer<$nativeType>");
    ArgumentError.checkNotNull(length, "length");
    _checkExternalTypedDataLength(length, $elementSize);
    _checkPointerAlignment(address, $elementSize);
    final result = _asExternalTypedData$nativeType(this, length);
    if (finalizer != null) {
      _attachAsTypedListFinalizer(finalizer, result, token ?? this, $sizeTimes length);
    }
    return result;
  }
""";

  switch (container) {
    case Container.pointer:
      buffer.write("""
@patch
extension ${nativeType}Pointer on Pointer<$nativeType> {
  @patch
  @pragma("vm:prefer-inline")
  $dartType get value => _load$nativeType(this, 0);

  @patch
  @pragma("vm:prefer-inline")
  set value($dartType value) => _store$nativeType(this, 0, value);

  @patch
  @pragma("vm:prefer-inline")
  $dartType operator [](int index) => _load$nativeType(this, ${sizeTimes}index);

  @patch
  @pragma("vm:prefer-inline")
  operator []=(int index, $dartType value) => _store$nativeType(this, ${sizeTimes}index, value);

$asTypedList
}

""");
    case Container.array:
      final String elementsGetter;
      final String listHelperClass;
      if (_isBool(nativeType)) {
        elementsGetter = """
  List<$dartType> get elements => _${nativeType}ArrayList(this);
""";
        listHelperClass = """
class _${nativeType}ArrayList with ListMixin<$dartType>, FixedLengthListMixin<$dartType> {
  _${nativeType}ArrayList(this._array);

  final Array<$nativeType> _array;

  @override
  int get length => _array._size;

  @override
  $dartType operator [](int index) => _array[index];

  @override
  operator []=(int index, $dartType value) {
    _array[index] = value;
  }
}
""";
      } else {
        elementsGetter = """
  $typedListType get elements {
    assert(_nestedDimensionsFlattened == 1);
    final length = _size;
    final typedDataBase = _typedDataBase;
    if (typedDataBase is Pointer) {
      return typedDataBase
          ._offsetBy(_offsetInBytes)
          .cast<$nativeType>()
          .asTypedList(length);
    }
    final typedData = _typedDataBase as TypedData;
    final start = typedData.offsetInBytes + _offsetInBytes;
    return $typedListType.view(typedData.buffer, start, length);
  }
""";
        listHelperClass = "";
      }

      buffer.write("""
@patch
extension ${nativeType}Array on Array<$nativeType> {
  @patch
  @pragma("vm:prefer-inline")
  $dartType operator [](int index) {
    _checkIndex(index);
    return _load$nativeType(_typedDataBase, _offsetInBytes + ${sizeTimes}index,);
  }

  @patch
  @pragma("vm:prefer-inline")
  operator []=(int index, $dartType value) {
    _checkIndex(index);
    return _store$nativeType(_typedDataBase, _offsetInBytes + ${sizeTimes}index , value,);
  }

  @patch
$elementsGetter
}
$listHelperClass

""");
    case Container.typedData:
    // Nothing to do, all rewritten in the CFE.
  }
}

final footer = """
//
// End of generated code.
//
""";

void generateFooter(StringBuffer buffer) {
  buffer.write(footer);
}

//
// Helper functions.
//

bool _isInt(String type) => type.startsWith("Int") || type.startsWith("Uint");
bool _isSigned(String type) => type.startsWith("Int");

bool _isBool(String type) => type == "Bool";

String sizeOf(int size) => "$size";

String sizeOfBits(int size) => "${size * 8}";

String sizeOfIntPtrSize(int size) => "$size";

final Uri _containingFolder = File.fromUri(Platform.script).parent.uri;

ArgParser argParser() {
  final parser = ArgParser(allowTrailingOptions: false);
  parser.addOption(
    'path',
    abbr: 'p',
    help: 'Path to generate the files at.',
    defaultsTo: _containingFolder.toString(),
  );
  parser.addFlag(
    'help',
    abbr: 'h',
    help: 'Display usage information.',
    callback: (help) {
      if (help) print(parser.usage);
    },
  );
  return parser;
}

class Config {
  final String nativeType;
  final String dartType;
  final String typedListType;
  final int elementSize;
  final Version? since;
  const Config(
    this.nativeType,
    this.dartType,
    this.typedListType,
    this.elementSize, {
    Version? since,
  }) : since = since;
}

const String kDoNotEmit = "donotemit";

class Version {
  final int major;
  final int minor;

  const Version(this.major, this.minor);

  @override
  String toString() => '$major.$minor';

  static Version? latest(Version? a, Version? b) {
    if (a == null) return b;
    if (b == null) return a;
    if (a.major > b.major) return a;
    if (b.major > a.major) return b;
    if (a.minor > b.minor) return a;
    return b;
  }

  String get sinceAnnotation => "@Since('$this')";
}
